package com.bankcard.core;

import com.bankcard.bininfo.model.BinInfo;
import com.bankcard.bininfo.BinInfoRepository;
import com.bankcard.card.AccountNumber;
import com.bankcard.card.ExpirationDate;
import com.bankcard.card.Name;
import com.bankcard.card.ServiceCode;
import com.bankcard.util.MaskingUtil;
import com.bankcard.util.CardNumberFormatter;
import com.bankcard.card.LuhnValidator;

import java.util.Objects;

/**
 * The {@code BankCard} class represents a bank card with various attributes such as account number,
 * expiration date, cardholder name, service code, and BIN information.
 * <p>
 * This class provides methods to access these attributes and ensures that the account number is
 * always present and valid.
 * </p>
 * <p>
 * Example usage:
 * </p>
 * <pre>
 * {@code
 * // Creating a BankCard instance with all attributes
 * AccountNumber accountNumber = new AccountNumber("1234567890123456");
 * ExpirationDate expirationDate = new ExpirationDate("12/23");
 * Name cardholderName = new Name("John Doe");
 * ServiceCode serviceCode = new ServiceCode("101");
 * BankCard bankCard = new BankCard(accountNumber, expirationDate, cardholderName, serviceCode);
 *
 * // Creating a BankCard instance with only account number
 * BankCard bankCardSimple = new BankCard("1234567890123456");
 *
 * // Creating a BankCard instance with all attributes as strings
 * BankCard bankCardString = new BankCard("1234567890123456", "12/23", "John Doe", "101");
 *
 * // Creating a BankCard instance using the static factory method
 * BankCard bankCardFrom = BankCard.ofAccountNumber("1234567890123456");
 *
 * // Validating a BankCard Number
 * boolean isValid = BankCard.isValid("1234567890123456");
 *
 * // Accessing attributes
 * AccountNumber accNum = bankCard.getAccountNumber();
 * ExpirationDate expDate = bankCard.getExpirationDate();
 * Name name = bankCard.getCardholderName();
 * ServiceCode svcCode = bankCard.getServiceCode();
 * BinInfo binInfo = bankCard.getBinInfo();
 * }
 * </pre>
 * <p>
 * The {@code BankCard} class also provides a static factory method {@code ofAccountNumber} to create an instance
 * using only the account number.
 * </p>
 */
public final class BankCard {
    private final AccountNumber accountNumber;
    private final ExpirationDate expirationDate;
    private final Name cardholderName;
    private final ServiceCode serviceCode;
    private final BinInfo binInfo;

    /**
     * Constructs a new {@code BankCard} instance with the specified account number, expiration date,
     * cardholder name, and service code.
     *
     * @param accountNumber the account number of the bank card
     * @param expirationDate the expiration date of the bank card
     * @param cardholderName the name of the cardholder
     * @param serviceCode the service code of the bank card
     * @throws NullPointerException if the account number is null
     */
    public BankCard(AccountNumber accountNumber, ExpirationDate expirationDate, Name cardholderName, ServiceCode serviceCode) {
        this.accountNumber = Objects.requireNonNull(accountNumber, "Account number cannot be null");
        this.expirationDate = expirationDate;
        this.cardholderName = cardholderName;
        this.serviceCode = serviceCode;
        this.binInfo = BinInfoRepository.getInstance().getBinInfo(accountNumber.getRawNumber());
    }

    /**
     * Constructs a new {@code BankCard} instance with the specified account number, expiration date,
     * cardholder name, and service code as strings.
     *
     * @param accountNumber the account number of the bank card
     * @param expirationDate the expiration date of the bank card
     * @param cardholderName the name of the cardholder
     * @param serviceCode the service code of the bank card
     */
    public BankCard(String accountNumber, String expirationDate, String cardholderName, String serviceCode) {
        this(
                new AccountNumber(accountNumber),
                expirationDate != null ? new ExpirationDate(expirationDate) : null,
                cardholderName != null ? new Name(cardholderName) : null,
                serviceCode != null ? new ServiceCode(serviceCode) : null
        );
    }

    /**
     * Constructs a new {@code BankCard} instance with the specified account number.
     *
     * @param accountNumber the account number of the bank card
     */
    public BankCard(String accountNumber) {
        this(
                new AccountNumber(accountNumber),
                null,
                null,
                null
        );
    }

    /**
     * Creates a new {@code BankCard} instance with the specified account number.
     *
     * @param accountNumber the account number of the bank card
     * @return a new {@code BankCard} instance
     */
    public static BankCard ofAccountNumber(String accountNumber) {
        return new BankCard(accountNumber);
    }

    public static String formatNumber(String accountNumber, String pattern) {
        return CardNumberFormatter.formatWithPattern(accountNumber, pattern);
    }

    public static boolean isValid(String accountNumber) {
        return LuhnValidator.isValid(accountNumber);
    }

    public String formatNumber(String pattern) {
        return this.accountNumber.getFormattedNumber(pattern);
    }

    /**
     * Finds the BIN information for the specified account number.
     *
     * @param accountNumber the account number of the bank card
     * @return the BIN information for the specified account number
     */
    public static BinInfo findBin(String accountNumber) {
        return BinInfoRepository.getInstance().getBinInfo(accountNumber);
    }

    public AccountNumber getAccountNumber() {
        return accountNumber;
    }

    public ExpirationDate getExpirationDate() {
        return expirationDate;
    }

    public Name getCardholderName() {
        return cardholderName;
    }

    public ServiceCode getServiceCode() {
        return serviceCode;
    }

    public BinInfo getBinInfo() {
        return binInfo;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BankCard bankCard = (BankCard) o;
        return accountNumber.equals(bankCard.accountNumber) &&
                Objects.equals(expirationDate, bankCard.expirationDate) &&
                Objects.equals(cardholderName, bankCard.cardholderName) &&
                Objects.equals(serviceCode, bankCard.serviceCode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNumber, expirationDate, cardholderName, serviceCode);
    }

    @Override
    public String toString() {
        return "BankCard{" +
                "accountNumber=" + MaskingUtil.maskCardNumber(accountNumber.toString()) +
                ", expirationDate=" + (expirationDate != null ? expirationDate : "N/A") +
                ", cardholderName=" + (cardholderName != null ? MaskingUtil.maskName(cardholderName.toString()) : "N/A") +
                ", serviceCode=" + (serviceCode != null ? serviceCode : "N/A") +
                ", binInfo=" + binInfo +
                '}';
    }

}